<?php

declare(strict_types=1);

namespace app\core\DB\Test;


use app\core\Db as MainDB;
use app\core\DB\Interfaces\IDb;
use app\core\DB\Interfaces\IStatement;
use PDO;
use RuntimeException;
use Swoole\Coroutine;
use Swoole\Database\PDOProxy;
use Swoole\Database\PDOStatementProxy;
use Throwable;
class Db
{
    protected $pool;

    /** @var PDO */
    protected $pdo;

    private $in_transaction = false;
    private static $instance=null;
    public function __construct()
    {
     
            $this->pool =MainDB::getInstance();
        
    }
    public static function getInstance(){
        if(null===static::$instance){
        
            static::$instance=new Self();
        }
        return static::$instance;
    }
    public  function ping(){
        $this->query('SELECT 1');
        // $this->pool->ping();
    }
    // public function quote(string $string, int $parameter_type = PDO::PARAM_STR)
    // {
    //     $this->realGetConn();
    //     try {
    //         $ret = $this->pdo->quote($string, $parameter_type);
    //     } catch (Throwable $th) {
    //         if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) {
    //             $this->release();
    //         } else {
    //             $this->release($this->pdo);
    //         }
    //         throw $th;
    //     }

    //     $this->release($this->pdo);
    //     return $ret;
    // }

    public function beginTransaction(): void
    {
        if ($this->in_transaction) { //嵌套事务
            throw new RuntimeException('do not support nested transaction now');
        }
        $this->realGetConn();
        try {
            $this->pdo->beginTransaction();
        } catch (Throwable $th) {
            if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) {
                $this->release();
            } else {
                $this->release($this->pdo);
            }
            throw $th;
        }
        $this->in_transaction = true;
        Coroutine::defer(function () {
            if ($this->in_transaction) {
                $this->rollBack();
            }
        });
    }

    public function commit(): void
    {
        $this->in_transaction = false;
        try {
            $this->pdo->commit();
        } catch (Throwable $th) {
            if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) {
                $this->release();
            } else {
                $this->release($this->pdo);
            }
            throw $th;
        }
        $this->release($this->pdo);
    }

    public function rollBack(): void
    {
        $this->in_transaction = false;

        try {
            $this->pdo->rollBack();
        } catch (Throwable $th) {
            if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) {
                $this->release();
            } else {
                $this->release($this->pdo);
            }
            throw $th;
        }

        $this->release($this->pdo);
    }

    public function query(string $query, array $bindings = []): array
    {
        $this->realGetConn();
        try {
            $statement = $this->pdo->prepare($query);

            $this->bindValues($statement, $bindings);

            $statement->execute();

            $ret = $statement->fetchAll();
        } catch (Throwable $th) {
            if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) {
                $this->release();
            } else {
                $this->release($this->pdo);
            }
            throw $th;
        }

        $this->release($this->pdo);

        return $ret;
    }
    
    public function fetch(string $query, array $bindings = [])
    {
        $records = $this->query($query, $bindings);
       
        return array_shift($records);
    }

    public function execute(string $query, array $bindings = []): int
    {
        $this->realGetConn();
        try {
            $statement = $this->pdo->prepare($query);

            $this->bindValues($statement, $bindings);

            $statement->execute();

            $ret = $statement->rowCount();
        } catch (Throwable $th) {
            if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) {
                $this->release();
            } else {
                $this->release($this->pdo);
            }
            throw $th;
        }

        $this->release($this->pdo);

        return $ret;
    }

    public function exec(string $sql): int
    {
        $this->realGetConn();
        try {
            $ret = $this->pdo->exec($sql);
        } catch (Throwable $th) {
            if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) {
                $this->release();
            } else {
                $this->release($this->pdo);
            }
            throw $th;
        }

        $this->release($this->pdo);

        return $ret;
    }

    public function insert(string $query, array $bindings = []): int
    {
        $this->realGetConn();

        try {
            $statement = $this->pdo->prepare($query);

            $this->bindValues($statement, $bindings);

            $statement->execute();

            $ret = (int) $this->pdo->lastInsertId();
        } catch (Throwable $th) {
            if (in_array($th->getCode(), PDOProxy::IO_ERRORS, true)) {
                $this->release();
            } else {
                $this->release($this->pdo);
            }
            throw $th;
        }

        $this->release($this->pdo);

        return $ret;
    }

    public function release($connection = null)
    {
        if ($connection === null) {
            $this->in_transaction = false;
        }

        if (! $this->in_transaction) {
            $this->pool->close($connection);
            return true;
        }

        return false;
    }

    protected function bindValues(PDOStatementProxy $statement, array $bindings): void
    {
        foreach ($bindings as $key => $value) {
            $mapValue=$this->typeMap($value,gettype($value));
            $statement->bindValue(
                is_string($key) ? $key : $key + 1,
                $mapValue[0],
                $mapValue[1]
            );
        }
    }

    private function realGetConn()
    {
        if (! $this->in_transaction) {
            $this->pdo = $this->pool->getConnection();
        }
    }
    protected function typeMap($value, $type)
    {
        $map = [
            'NULL' => PDO::PARAM_NULL,
            'integer' => PDO::PARAM_INT,
            'double' => PDO::PARAM_STR,
            'boolean' => PDO::PARAM_BOOL,
            'string' => PDO::PARAM_STR,
            'object' => PDO::PARAM_STR,
            'resource' => PDO::PARAM_LOB,
        ];

        if ($type === 'boolean') {
            $value = ($value ? '1' : '0');
        } elseif ($type === 'NULL') {
            $value = null;
        }

        return [$value, $map[$type]];
    }
}
